# plugs/collective.py
#
# 

""" collective connections """

__copyright__ = 'this file is in the public domain'

from gozerbot.generic import rlog, geturl, handle_exception
from gozerbot.rsslist import rsslist
from gozerbot.datadir import datadir
from gozerbot.persist import Persist
from gozerbot.commands import cmnds
from gozerbot.plugins import plugins
from gozerbot.thr import start_new_thread
from gozerbot.plughelp import plughelp
from gozerbot.examples import examples
from xml.sax.saxutils import unescape
import Queue, time, socket, re

plughelp.add('collective', 'manage links to other bots .. to be accessible a \
bot needs to run the webserver')

coll = Persist(datadir + '/collective')

if not coll.data:
    coll.data = {}
if not coll.data.has_key('enable'):
    coll.data['enable'] = 0
if not coll.data.has_key('nodes'):
    coll.data['nodes'] = []
if not coll.data.has_key('names'):
    coll.data['names'] = {}
if not coll.data.has_key('wait'):
    coll.data['wait'] = 5

def init():
    """ init the collective plugin """
    if not coll.data['enable']:
        return 1
    start_new_thread(boot, ())
    return 1

def getnodes():
    """ return cached nodes cache """
    return coll.data['nodes']

def addnode(node):
    """ add a node to cache """
    (host, port) = node.split(':')
    try:
        ipnr = socket.gethostbyname(host)
        if ipnr:
            node = "%s:%s" % (ipnr, port)
    except:
        pass
    if node not in coll.data['nodes']:
        rlog(0, 'collective', 'adding node %s' % str(node))
        coll.data['nodes'].append(node)
        coll.save()
        return 1
    return 0

def syncnode(node):
    """ sync cacne with node """
    try:
        result = geturl('http://%s/nodes&how=noescape' % node)
    except:
        rlog(10, 'collective', "can't fetch %s data" % node)
        return 0
    rss = rsslist(result)
    got = 0
    for i in rss:
        try:
            node = i['node']
            if addnode(node):
                got = 1
        except:
            handle_exception()
            continue
        try:
            if not coll.data['names'].has_key(node):
                coll.data['names'][node] = i['name']
        except:
            pass
    if got:
        coll.save()
    return 1

def colldispatch(node, what, queue):
    """ dispatch command on remote node .. place result in queue """
    try:
        what = re.sub('\s+', '+', what)
        result = geturl('http://%s/dispatch?%s&how=direct' % (node, what))
    except:
        rlog(10, 'collective', "can't fetch %s data" % node)
        return 0
    if coll.data['names'].has_key(node):
        name = coll.data['names'][node]
    else:
        name = node
    res = result.split('\n\n')
    if len(res) > 1:
        for i in res:
            if not i:
                continue
            queue.put((name, i))
    else:
        queue.put((name, res[0]))

def colldispatchall(what):
    """ coll dispatch on all nodes """
    queue = Queue.Queue()
    for i in coll.data['nodes']:
        start_new_thread(colldispatch, (i, what, queue))
    for i in range(coll.data['wait']):
        if queue.qsize() == len(coll.data['nodes']):
            break
        time.sleep(1)
    queue.put(None)
    return queue
        
def boot(server=None):
    """ sync cache with main server or server provided """
    if not server:
        syncnode('r8.cg.nu:8088')
    else:
        syncnode(server)
    nrnodes = len(coll.data['nodes'])
    rlog(10, 'collective', 'booted %s nodes' % nrnodes)
    return nrnodes
    
def nodesxml():
    """ return nodes in xml format """
    result = "<xml>\n"
    gotit = False
    for i in coll.data['nodes']:
        gotit = True
        result += "    <coll>\n"
        result += "        <node>%s</node>\n" % i
        if coll.data['names'].has_key(i):
            result += "        <name>%s</name>\n" % coll.data['names'][i]
        result += "    </coll>\n"
    if gotit:
        result += "</xml>"
        return result
    return ""
        
def handle_colllist(bot, ievent):
    """ coll-list .. list all nodes in cache """
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    ievent.reply(str(coll.data['nodes']))

cmnds.add('coll-list', handle_colllist, 'OPER')
examples.add('coll-list', 'list nodes cache', 'coll-list')

def handle_collenable(bot, ievent):
    """ coll-enable .. enable the collective """
    coll.data['enable'] = 1
    coll.save()
    plugins.reload('plugs', 'collective')
    ievent.reply('collective enabled')

cmnds.add('coll-enable', handle_collenable, 'OPER')
examples.add('coll-enable', 'enable the collective', 'coll-enable')

def handle_colldisable(bot, ievent):
    """ coll-disable .. disable the collective """
    coll.data['enable'] = 0
    coll.save()
    plugins.reload('plugs', 'collective')
    ievent.reply('collective disabled')

cmnds.add('coll-disable', handle_colldisable, 'OPER')
examples.add('coll-disable', 'disable the collective', 'coll-disable')

def handle_collsync(bot, ievent):
    """ coll-sync <node> .. sync nodes cache with node """ 
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    try:
        node = ievent.args[0]
    except IndexError:
        ievent.missing('<node>')
        return
    result = syncnode(node)
    ievent.reply('%s nodes added' % result)

cmnds.add('coll-sync', handle_collsync, 'OPER')
examples.add('coll-sync', 'coll-sync <node> .. sync with provided node', \
'coll-sync r8.cg.nu')

def handle_collexec(bot, ievent):
    """ coll <cmnd> .. execute <cmnd> on nodes """
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    if not ievent.rest:
        ievent.missing('<command>')
        return
    result = colldispatchall(ievent.rest)
    resultstr = ""
    total = len(coll.data['nodes'])
    teller = 0
    got = []
    while 1:
        try:
            res = result.get(1, coll.data['wait'])
        except socket.timeout:
            break
        if not res:
            break
        resultstr += "|%s| %s " % (res[0], unescape(res[1]))
        if res[0] not in got:
            teller += 1
            got.append(res[0])
    if resultstr:
        ievent.reply('%s out of %s ==> %s' % (teller, total, resultstr))
    else:
        ievent.reply('no results found')

cmnds.add('coll', handle_collexec, ['USER', 'WEB'])
examples.add('coll', 'coll <cmnd> .. execute command in the collective', \
'coll lq')

def handle_colladdnode(bot, ievent):
    """ coll-addnode <name> <host:port> .. add node to cache """
    try:
        (name, node) = ievent.args
    except ValueError:
        ievent.missing('<name> <host:port>')
        return
    if ':' not in node:
        ievent.missing('<name> <host:port>')
        return
    (host, port) = node.split(':')
    try:
        ipnr = socket.gethostbyname(host)
        if ipnr:
            node = "%s:%s" % (ipnr, port)
    except:
        ievent.reply("can't find ipnr for %s" % host)
        return
    coll.data['names'][node] = name
    addnode(node)
    coll.save()
    ievent.reply('%s added' % name)

cmnds.add('coll-addnode', handle_colladdnode, 'OPER')
examples.add('coll-addnode', 'coll-addnode <name> <node> .. add a node to \
cache .. node is in host:port format', 'coll-addnode r8 r8.cg.nu:8088')

def handle_colldelnode(bot, ievent):
    """ coll-delnode <name> .. delete node from cache """
    try:
        what = ievent.args[0]
    except IndexError:
        ievent.missing('<name>')
        return
    try:
        for i, j in coll.data['names'].iteritems():
            if j == what:
                coll.data['nodes'].remove(i)
                coll.save()
                ievent.reply('%s deleted' % what)
                break
        ievent.reply("can't find node %s" % what)
    except ValueError:
        ievent.reply('failed to remove %s' % what)

cmnds.add('coll-delnode', handle_colldelnode, 'OPER')
examples.add('coll-delnode', 'coll-delnode <name> .. remove node from \
collective list', 'coll-delnode r8')

def handle_collsetname(bot, ievent):
    """ coll-setname <node> <name> .. set name of node """
    try:
        (what, name) = ievent.args
    except ValueError:
        ievent.missing('<node> <name>')
        return
    coll.data['names'][what] = name
    coll.save()
    ievent.reply('%s added' % name)
 
cmnds.add('coll-setname', handle_collsetname, 'OPER')
examples.add('coll-setname', 'set name of collective node', 'coll-setname \
r8 r8.cg.nu:8088')

def handle_collnames(bot, ievent):
    """ coll-names .. show names with nodes in cache """
    ievent.reply(str(coll.data['names']))
 
cmnds.add('coll-names', handle_collnames, 'OPER')

def handle_collboot(bot, ievent):
    """ boot the collective node cache """
    try:
        server = ievent.args[0]
    except:
        server = 'r8.cg.nu:8088'
    bootnr = boot(server)
    ievent.reply('collective at %s nodes' % bootnr)
 
cmnds.add('coll-boot', handle_collboot, 'OPER')
examples.add('coll-boot', 'sync collective list with provided host', \
'coll-boot')

def handle_collfullboot(bot, ievent):
    """ coll-fullboot .. boot from all nodes in cache """
    try:
        (server, port) = ievent.args
    except ValueError:
        server = 'r8.cg.nu'
        port = 8088
    teller = 0
    snap = list(coll.data['nodes'])
    for i in snap:
        boot(i)
        teller += 1
    ievent.reply('%s nodes checked .. current %s nodes in list' % (teller, 
len(coll.data['nodes'])))
 
cmnds.add('coll-fullboot', handle_collfullboot, 'OPER')
examples.add('coll-fullboot', 'do a boot on every node in the collective \
list', 'coll-fullboot')

def handle_collsave(bot, ievent):
    """ coll-save .. save collective data to disk """
    coll.save()
    ievent.reply('collective saved')
 
cmnds.add('coll-save', handle_collsave, 'OPER')
examples.add('coll-save', 'save collective data' , 'coll-save')

def handle_clean(bot , ievent):
    """ coll-clean .. clear nodes cache """
    coll.data['nodes'] = []
    coll.save()
    ievent.reply('done')

cmnds.add('coll-clean', handle_clean, 'OPER')
examples.add('coll-clean', 'clean the collective list', 'coll-clean')
